package umage.antlr;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;
import java.lang.reflect.Method;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.antlr.v4.runtime.ANTLRInputStream;
import org.antlr.v4.runtime.CharStream;
import org.antlr.v4.runtime.CommonTokenStream;
import org.antlr.v4.runtime.ParserRuleContext;
import org.antlr.v4.runtime.tree.ParseTreeWalker;
import org.antlr.v4.runtime.tree.TerminalNode;

import umage.antlr.Java7Parser.*;

public abstract class JavaUnit implements Serializable, Cloneable {
	public enum Modifier {PUBLIC, PROTECTED, PRIVATE, STATIC, ABSTRACT, FINAL, NATIVE, SYNCHRONIZED, TRANSIENT, VOLATILE, STRICTFP};
	
	protected String name;
	protected EnumSet<Modifier> modifiers;
	protected JavaUnit parent;
	
	protected JavaUnit() {}
	
	public String getName() {
		return name;
	}
	
	public EnumSet<Modifier> getModifiers() {
		return modifiers;
	}
	
	public JavaUnit getParent() {
		return parent;
	}
	
	protected static class ParseCore extends Java7BaseListener {
		protected Java7Parser parser;
		protected InterfaceUnit result;
		protected Set<String> preclassRefs;
		protected int anonLevel = 0; //variable used for ignoring anonomous declarations that don't fit in UML
		
		protected String packageName = "";
		
		protected ParseCore(Java7Parser p) {
			parser = p;
		}
		
		protected InterfaceUnit getUnit() {
			return result;
		}
		
		@Override
		public void enterPackageDeclaration(PackageDeclarationContext ctx) {
			List<TerminalNode> packageParts = ctx.qualifiedIdentifier().Identifier();
			StringBuilder packageComponents = new StringBuilder();
			for (TerminalNode packagePart : packageParts) {
				packageComponents.append(packagePart.getText());
				packageComponents.append('.');
			}
			packageName = packageComponents.toString();
			packageName = packageName.substring(0, packageName.length() - 1);
		}
		
		@Override
		public void enterNormalClassDeclaration(
				NormalClassDeclarationContext ctx) {
			ClassUnit newUnit = new ClassUnit();
			newUnit.packageName = packageName;
			newUnit.name = ctx.Identifier().toString();
			newUnit.modifiers = createModifierSet(ctx);
			if (ctx.typeList() != null) {
				List<ClassOrInterfaceTypeContext> interfaces = ctx.typeList().classOrInterfaceType();
				for (ClassOrInterfaceTypeContext interfaceVal : interfaces) {
					newUnit.interfaceNames.add(interfaceVal.getText());
				}
			}
			if (ctx.typeRef() != null) {
				newUnit.superClassName = ctx.typeRef().classOrInterfaceType().getText();
			} else newUnit.superClassName = null;
			if (result != null) {
				newUnit.parent = result;
				result.innerUnits.add(newUnit);
			}
			result = newUnit;
			if (preclassRefs != null) {
				result.referencedUnits.addAll(preclassRefs);
				preclassRefs = null;
			}
		}
		
		@Override
		public void enterNormalInterfaceDeclaration(
				NormalInterfaceDeclarationContext ctx) {
			InterfaceUnit newUnit = new InterfaceUnit();
			newUnit.packageName = packageName;
			newUnit.name = ctx.Identifier().toString();
			newUnit.modifiers = createModifierSet(ctx);
			if (ctx.typeList() != null) {
				List<ClassOrInterfaceTypeContext> interfaces = ctx.typeList().classOrInterfaceType();
				for (ClassOrInterfaceTypeContext interfaceVal : interfaces) {
					newUnit.interfaceNames.add(interfaceVal.getText());
				}
			}
			if (result != null) {
				newUnit.parent = result;
				result.innerUnits.add(newUnit);
			}
			result = newUnit;
			if (preclassRefs != null) {
				result.referencedUnits.addAll(preclassRefs);
				preclassRefs = null;
			}
		}
		
		@Override
		public void exitNormalClassDeclaration(NormalClassDeclarationContext ctx) {
			if (result.parent != null) {
				result = (InterfaceUnit)result.parent;
			}
		}
		
		@Override
		public void exitNormalInterfaceDeclaration(
				NormalInterfaceDeclarationContext ctx) {
			if (result.parent != null) {
				result = (InterfaceUnit)result.parent;
			}
		}
		
		@Override
		public void enterClassOrInterfaceType(ClassOrInterfaceTypeContext ctx) {
			for (TerminalNode id : ctx.Identifier()) {
				if (result != null) result.referencedUnits.add(id.getText());
				else {
					if (preclassRefs == null) preclassRefs = new HashSet<String>();
					preclassRefs.add(id.getText());
				}
			}
		}
		
		@Override
		public void enterFieldDeclaration(FieldDeclarationContext ctx) {
			if (anonLevel == 0) {
				String type = stripPackage(parser.getTokenStream().getText(ctx.typeRef().getSourceInterval()));
				EnumSet<Modifier> modifiers = createModifierSet(ctx);
				for (VariableDeclaratorContext var : ctx.variableDeclarators().variableDeclarator()) {
					FieldUnit newField = new FieldUnit();
					newField.name = var.variableDeclaratorId().getText();
					newField.modifiers = modifiers;
					newField.parent = result;
					newField.type = type;
					result.fields.add(newField);
				}
			}
			anonLevel++;
		}
		
		@Override
		public void exitFieldDeclaration(FieldDeclarationContext ctx) {
			anonLevel--;
		}
		
		@Override
		public void enterMethodDeclaration(MethodDeclarationContext ctx) {
			if (anonLevel == 0) {
				MethodUnit newMethod = new MethodUnit();
				int methodNamePos;
				if (ctx.typeRef() != null) methodNamePos = ctx.typeRef().getStop().getTokenIndex() + 2;
				else methodNamePos = ctx.getStart().getTokenIndex() + 2;
				newMethod.name = parser.getTokenStream().get(methodNamePos).getText();
				newMethod.modifiers = createModifierSet(ctx);
				newMethod.parent = result;
				if (ctx.typeRef() != null) newMethod.returnType = stripPackage(parser.getTokenStream().getText(ctx.typeRef().getSourceInterval()));
				else newMethod.returnType = null;
				if (ctx.formalParameters() != null) {
					FormalParameterDeclarationsContext var = ctx.formalParameters().formalParameterDeclarations();
					while (var != null) {
						FieldUnit newPara = new FieldUnit();
						newPara.name = var.formalParameterVariables().variableDeclaratorId().getText();
						newPara.type = stripPackage(parser.getTokenStream().getText(var.typeRef().getSourceInterval()));
						newPara.modifiers = EnumSet.noneOf(Modifier.class);
						List<VariableModifierContext> mods = var.variableModifier();
						for (VariableModifierContext mod : mods) {
							newPara.modifiers.add(Modifier.valueOf(mod.getText().toUpperCase()));
						}
						newPara.parent = newMethod;
						newMethod.parameters.add(newPara);
						var = var.formalParameterVariables().formalParameterDeclarations();
					}
				}
				result.methods.add(newMethod);
			}
			anonLevel++;
		}
		
		@Override
		public void exitMethodDeclaration(MethodDeclarationContext ctx) {
			anonLevel--;
		}
		
		@Override
		public void enterConstructorDeclaration(
				ConstructorDeclarationContext ctx) {
			if (anonLevel == 0) {
				MethodUnit newMethod = new MethodUnit();
				newMethod.name = result.name;
				newMethod.modifiers = createModifierSet(ctx);
				newMethod.parent = result;
				newMethod.returnType = null;
				if (ctx.formalParameters() != null) {
					FormalParameterDeclarationsContext var = ctx.formalParameters().formalParameterDeclarations();
					while (var != null) {
						FieldUnit newPara = new FieldUnit();
						newPara.name = var.formalParameterVariables().variableDeclaratorId().getText();
						newPara.type = stripPackage(parser.getTokenStream().getText(var.typeRef().getSourceInterval()));
						newPara.modifiers = EnumSet.noneOf(Modifier.class);
						List<VariableModifierContext> mods = var.variableModifier();
						for (VariableModifierContext mod : mods) {
							newPara.modifiers.add(Modifier.valueOf(mod.getText().toUpperCase()));
						}
						newPara.parent = newMethod;
						newMethod.parameters.add(newPara);
						var = var.formalParameterVariables().formalParameterDeclarations();
					}
				}
				result.methods.add(newMethod);
			}
			anonLevel++;
		}
		
		@Override
		public void exitConstructorDeclaration(ConstructorDeclarationContext ctx) {
			anonLevel--;
		}
		
		protected EnumSet<Modifier> createModifierSet(ParserRuleContext ctx) {
			EnumSet<Modifier> result = EnumSet.noneOf(Modifier.class);
			do {
				try {
					Method modifierMethod = ctx.getClass().getMethod("modifier");
					@SuppressWarnings("unchecked")
					List<ModifierContext> modifiers = (List<ModifierContext>) modifierMethod.invoke(ctx);
					for (ModifierContext modifier : modifiers) {
						try {
							result.add(Modifier.valueOf(modifier.getText().toUpperCase()));
						} catch (Exception ex) {}
					}
					break;
				} catch (NoSuchMethodException ex) {
				} catch (Exception ex) {ex.printStackTrace();}
			} while ((ctx = ctx.getParent()) != null);
			return result;
		}
		
		protected String stripPackage(String input) {
			int cutoff = input.lastIndexOf('.');
			if (cutoff == -1) return input;
			else return input.substring(cutoff + 1);
		}
		
	}
	
	public static InterfaceUnit parseUnit(File file) {
		InputStream stream = null;
		try {
			stream = new BufferedInputStream(new FileInputStream(file));
			CharStream cs = new ANTLRInputStream(stream);
			Java7Lexer lexer = new Java7Lexer(cs);
			CommonTokenStream tokens = new CommonTokenStream(lexer);
			Java7Parser parser = new Java7Parser(tokens);
			ParserRuleContext tree = parser.compilationUnit(); // parse
			
			ParseTreeWalker walker = new ParseTreeWalker();
			ParseCore core = new ParseCore(parser);
			walker.walk(core, tree);
			
			return core.getUnit();
		} catch (Exception ex) {
			ex.printStackTrace();
			return null;
		} finally {
			if (stream != null) {
				try { stream.close(); } catch (Exception e) {}
			}
		}
		
	}
	
}
